package com.study.app.service.impl;

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.study.app.mapper.AppUserMapper;
import com.study.app.service.IAppLoginService;
import com.study.common.constant.CacheConstants;
import com.study.common.constant.Constants;
import com.study.common.core.domain.event.LogininforEvent;
import com.study.common.core.domain.model.AppEmailLoginBody;
import com.study.common.core.domain.model.AppLoginUser;
import com.study.common.core.domain.model.AppSmsLoginBody;
import com.study.common.core.domain.model.AppThirdLoginBody;
import com.study.common.enums.AppUserStatus;
import com.study.common.enums.LoginType;
import com.study.common.enums.UserType;
import com.study.common.exception.ServiceException;
import com.study.common.exception.user.UserException;
import com.study.common.helper.AppLoginHelper;
import com.study.common.utils.*;
import com.study.common.utils.redis.RedisUtils;
import com.study.common.utils.spring.SpringUtils;
import com.study.app.domain.AppUser;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.function.Supplier;

/**
 * App用户登录Service业务层处理
 *
 * @author study
 * @date 2023-04-19
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class AppLoginServiceImpl implements IAppLoginService {

    private final AppUserMapper appUserMapper;

    @Value("${user.password.maxRetryCount:5}")
    private Integer maxRetryCount;

    @Value("${user.password.lockTime:10}")
    private Integer lockTime;

    @Value("${rsa.privateKey:#{null}}")
    private String privateKey;

    @Override
    public boolean checkPhone(String areaCode, String phoneNumber) {
        AppUser appUser = appUserMapper.selectOne(Wrappers.<AppUser>query().lambda()
                .eq(AppUser::getAreaCode, areaCode)
                .eq(AppUser::getUserPhone, AesGcmUtils.encrypt(phoneNumber)));
        return appUser != null;
    }

    @Override
    public String smsLogin(AppSmsLoginBody appSmsLoginBody) {
        // 获取用户信息，并校验
        AppUser user = loadUserByUsername(appSmsLoginBody.getPhoneNumber());

        checkLogin(LoginType.SMS, user.getId(), appSmsLoginBody.getPhoneNumber(), () -> checkSmsCode(appSmsLoginBody.getPhoneNumber(), appSmsLoginBody.getCode()));
        // 此处可根据登录用户的数据不同 自行创建 loginUser
        AppLoginUser loginUser = buildLoginUser(user);
        // 生成token
        AppLoginHelper.login(loginUser);

        // 记录登录信息
        recordLogininfor(appSmsLoginBody.getPhoneNumber(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));

        return StpUtil.getTokenValue();
    }

    @Override
    public String emailLogin(AppEmailLoginBody appEmailLoginBody) {
        // 获取用户信息，并校验
        AppUser user = loadUserByUsername(appEmailLoginBody.getEmail());
        checkLogin(LoginType.EMAIL, user.getId(), appEmailLoginBody.getEmail(), () -> AesGcmUtils.check(appEmailLoginBody.getPassword(), user.getUserPassword()));

        // 此处可根据登录用户的数据不同 自行创建 loginUser
        AppLoginUser loginUser = buildLoginUser(user);
        // 生成token
        AppLoginHelper.login(loginUser);

        // 记录登录信息
        recordLogininfor(appEmailLoginBody.getEmail(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));

        return StpUtil.getTokenValue();
    }

    @Override
    public boolean checkEmail(String email) {
        AppUser appUser = appUserMapper.selectOne(Wrappers.<AppUser>query().lambda()
                .eq(AppUser::getUserEmail, AesGcmUtils.encrypt(email)));
        return appUser != null;
    }

    /**
     * 校验验证码
     *
     * @param phoneNumber 手机号码
     * @param code        验证码
     * @return boolean
     */
    public boolean checkSmsCode(String phoneNumber, String code) {
        String key = RedisKeyUtils.getAppSmsLoginCode(phoneNumber);
        String value = RedisUtils.getCacheObject(key);
        boolean bol = code.equals(value);
        // 删除key
        if (bol) {
            RedisUtils.deleteObject(key);
        }

        return bol;
    }

    @Override
    public String login(String username, String password) {
        // 获取用户信息
        AppUser user = loadUserByUsername(username);

        checkLogin(LoginType.PASSWORD, user.getId(), username, () -> AesGcmUtils.check(password, user.getUserPassword()));
        // 此处可根据登录用户的数据不同 自行创建 loginUser
        AppLoginUser loginUser = buildLoginUser(user);
        // 生成token
        AppLoginHelper.login(loginUser);

        // 记录登录信息
        recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));

        return StpUtil.getTokenValue();
    }

    @Override
    public String thirdLogin(LoginType loginType, AppThirdLoginBody appThirdLoginBody) {
        LambdaQueryWrapper<AppUser> lambda = Wrappers.<AppUser>query().lambda();
        switch (loginType) {
            case QQ:
                lambda.eq(AppUser::getQqOpenid, appThirdLoginBody.getOpenId());
                break;
            case WECHAT:
                lambda.eq(AppUser::getWxOpenid, appThirdLoginBody.getOpenId());
                break;
            case APPLE_ID:
                lambda.eq(AppUser::getIphoneId, appThirdLoginBody.getOpenId());
                break;
            default:
                log.info("第三方其他登录方式暂未实现");
                break;
        }
        AppUser user = appUserMapper.selectOne(lambda);

        if (ObjectUtil.isNull(user)) {
            log.info("第三方登录用户不存在.");
            throw new UserException("user.thirdLogin.not.exists");
        } else if (AppUserStatus.DISABLE.getCode().equals(user.getStatus())) {
            log.info("第三方登录用户已被停用.");
            throw new UserException("user.thirdLogin.blocked");
        }

        checkLogin(loginType, user.getId(), user.getUserPhone(), () ->
                checkOpenId(loginType, user, appThirdLoginBody.getOpenId()));

        // 此处可根据登录用户的数据不同 自行创建 loginUser
        AppLoginUser loginUser = buildLoginUser(user);
        // 生成token
        AppLoginHelper.login(loginUser);
        // 记录登录信息
        recordLogininfor(user.getUserPhone(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));

        return StpUtil.getTokenValue();
    }

    private boolean checkOpenId(LoginType loginType, AppUser user, String openId) {
        String currentOpenId = "";
        switch (loginType) {
            case QQ:
                currentOpenId = user.getQqOpenid();
                break;
            case WECHAT:
                currentOpenId = user.getWxOpenid();
                break;
            case APPLE_ID:
                currentOpenId = user.getIphoneId();
                break;
            default:
                log.info("暂未实现");
                break;
        }
        return currentOpenId.equals(openId);
    }

    /**
     * 加载用户信息
     *
     * @param username 用户名
     * @return 用户信息AppUser
     */
    private AppUser loadUserByUsername(String username) {
        String encrypt = AesGcmUtils.encrypt(username);
        // 手机号码 邮箱
        AppUser user = appUserMapper.selectOne(new LambdaQueryWrapper<AppUser>()
                .eq(AppUser::getUserPhone, encrypt)
                .or()
                .eq(AppUser::getUserEmail, encrypt)
                .or()
                .eq(AppUser::getLoginAccount,encrypt));

        if (ObjectUtil.isNull(user)) {
            log.info("登录用户：{} 不存在.", username);
            throw new UserException("user.not.exists", username);
        } else if (AppUserStatus.DISABLE.getCode().equals(user.getStatus())) {
            log.info("登录用户：{} 已被停用.", username);
            throw new UserException("user.blocked", username);
        }
        return user;
    }

    /**
     * 登录校验
     */
    private void checkLogin(LoginType loginType, Long id, String username, Supplier<Boolean> supplier) {
        String errorKey = CacheConstants.APP_PWD_ERR_CNT_KEY + id;
        String loginFail = Constants.LOGIN_FAIL;

        // 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
        Integer errorNumber = RedisUtils.getCacheObject(errorKey);

        // 锁定时间内登录 则踢出
        if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
            recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
            throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
        }

        if (!supplier.get()) {
            // 是否第一次
            errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
            // 达到规定错误次数 则锁定登录
            if (errorNumber.equals(maxRetryCount)) {
                RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
                recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
                throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
            } else {
                // 未达到规定错误次数 则递增
                RedisUtils.setCacheObject(errorKey, errorNumber);
                recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
                throw new UserException(loginType.getRetryLimitCount(), errorNumber);
            }
        }

        // 登录成功 清空错误次数
        RedisUtils.deleteObject(errorKey);
    }

    /**
     * 记录登录信息
     *
     * @param username 用户名
     * @param status   状态
     * @param message  消息内容
     */
    private void recordLogininfor(String username, String status, String message) {
        LogininforEvent logininforEvent = new LogininforEvent();
        logininforEvent.setUsername(username);
        logininforEvent.setStatus(status);
        logininforEvent.setMessage(message);
        logininforEvent.setRequest(ServletUtils.getRequest());
        SpringUtils.context().publishEvent(logininforEvent);
    }

    /**
     * 构建登录用户
     */
    private AppLoginUser buildLoginUser(AppUser user) {
        AppLoginUser loginUser = new AppLoginUser();
        loginUser.setUserId(user.getId());
        loginUser.setUserCode(user.getUserCode());
        loginUser.setUserType(UserType.APP_USER.getUserType());
        loginUser.setAreaCode(user.getAreaCode());
        loginUser.setUserPhone(user.getUserPhone());
        loginUser.setUserEmail(user.getUserEmail());
        loginUser.setToken(StpUtil.getTokenValue());
        loginUser.setLoginTime(System.currentTimeMillis());
        return loginUser;
    }

    @Override
    public void logout() {
        try {
            AppLoginUser loginUser = AppLoginHelper.getLoginUser();
            // 退出登录
            StpUtil.logout();
            recordLogininfor(loginUser.getUserPhone(),
                    Constants.LOGOUT,
                    MessageUtils.message("user.logout.success"));
        } catch (NotLoginException ignored) {
        }
    }

    @SneakyThrows
    @Override
    public AppUser getAppUser(String username) {

        String encrypt = AesGcmUtils.encrypt(username);
        // 手机号码 邮箱
        AppUser user = appUserMapper.selectOne(new LambdaQueryWrapper<AppUser>()
                .eq(AppUser::getUserPhone, encrypt)
                .or()
                .eq(AppUser::getUserEmail, encrypt)
                .or()
                .eq(AppUser::getLoginAccount,encrypt));

        return user;
    }

    @Override
    public String decrypt(String data) {
        try {
            return RsaUtils.privateDecrypt(data, privateKey);
        } catch (Exception e) {
            throw new ServiceException(MessageUtils.message("info.tampered"));
        }
    }

}
